﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization;
using System.Web.UI;
using System.Web.UI.WebControls;

namespace HeroldIT.Dnn.JQuery.UI
{
    /// <summary>
    /// Creates a jQuery UI Slider widget.
    /// </summary>
    [ParseChildren(true)]
    public class Slider : TextBox
    {
        #region properties

        /// <summary>
        /// Gets or sets the name of the theme to use for the control.
        /// </summary>
        public string ThemeName { get; set; }

        /// <summary>
        /// Gets the client id for the actual slider control.
        /// </summary>
        public string SliderClientId
        {
            get
            {
                return this.ClientID + this.ClientIDSeparator + "slider";
            }
        }

        private double? setValue;

        private double parsedValue;

        /// <summary>
        /// Gets or sets the value of the slider if not in range mode.
        /// </summary>
        public double Value
        {
            get
            {
                if (this.setValue.HasValue)
                    return this.SanitizeValue(this.setValue.Value);

                this.TryParseValue();
                return this.SanitizeValue(this.parsedValue);
            }
            set
            {
                this.setValue = value;
            }
        }

        private double? setLowValue;

        private double parsedLowValue;

        /// <summary>
        /// Gets or sets the lower value of the slider when in range mode.
        /// </summary>
        public double LowValue
        {
            get
            {
                if (this.setLowValue.HasValue)
                    return this.SanitizeValue(this.setLowValue.Value);

                this.TryParseLowHighValue();
                return this.parsedLowValue;
            }
            set
            {
                this.setLowValue = value;
            }
        }

        private double? setHighValue;

        private double parsedHighValue;

        /// <summary>
        /// Gets or sets the higher value of the slider when in range mode.
        /// </summary>
        public double HighValue
        {
            get
            {
                if (this.setHighValue.HasValue)
                    return this.SanitizeValue(this.setHighValue.Value);

                this.TryParseLowHighValue();
                return this.parsedHighValue;
            }
            set
            {
                this.setHighValue = value;
            }
        }

        /// <summary>
        /// Gets or sets whether to automatically register jQuery UI on the page.
        /// </summary>
        [Browsable(true)]
        [Localizable(false)]
        [EditorBrowsable(EditorBrowsableState.Always)]
        [Category("Behavior")]
        [DefaultValue(false)]
        public bool AutoRegister { get; set; }

        #endregion

        #region jQuery UI settings properties

        /// <summary>
        /// Gets or sets whether to slide the handle smoothly when a user clicks
        /// outside the handle on the bar.
        /// </summary>
        [Setting("animate")]
        [DefaultValue(false)]
        public bool Animate { get; set; }

        private double max = 100d;

        /// <summary>
        /// Gets or sets the maximum value of the slider.
        /// </summary>
        [Setting("max")]
        [DefaultValue(100d)]
        public double Max
        {
            get
            {
                return this.max;
            }
            set
            {
                this.max = Math.Max(this.min, value);
                this.min = Math.Min(this.min, value);
            }
        }

        private double min;

        /// <summary>
        /// Gets or sets the minimum value of the slider.
        /// </summary>
        [Setting("min")]
        [DefaultValue(0d)]
        public double Min
        {
            get
            {
                return this.min;
            }
            set
            {
                this.min = Math.Min(this.max, value);
                this.max = Math.Max(this.max, value);
            }
        }

        private SliderOrientation orientation = SliderOrientation.Auto;

        /// <summary>
        /// Gets or sets the orientation for the slider control.
        /// </summary>
        /// <remarks>
        /// Normally you don't need to set this option because the plugin detects the slider
        /// orientation automatically. If the orientation is not correctly detected you can set
        /// this option to <see cref="SliderOrientation.Horizontal"/> or <see cref="SliderOrientation.Vertical"/>.
        /// </remarks>
        [Setting("orientation")]
        [DefaultValue(SliderOrientation.Auto)]
        public SliderOrientation Orientation
        {
            get { return this.orientation; }
            set { this.orientation = value; }
        }

        private SliderRange range = SliderRange.None;

        /// <summary>
        /// If set to <see cref="SliderRange.Range"/>, the slider will detect if you have two
        /// handles and create a stylable range element between these two. Two other possible
        /// values are <see cref="SliderRange.Min"/> and <see cref="SliderRange.Max"/>. A min
        /// range goes from the slider min to one handle. A max range goes from one handle to
        /// the slider max. The default is <see cref="SliderRange.None"/>.
        /// </summary>
        [Setting("range")]
        [DefaultValue(SliderRange.None)]
        public SliderRange Range
        {
            get { return this.range; }
            set { this.range = value; }
        }

        private double step = 1d;

        /// <summary>
        /// Gets or sets the size or amount of each interval or step the slider takes between
        /// min and max. The full specified value range of the slider (max - min) needs to be
        /// evenly divisible by the step.
        /// </summary>
        [Setting("step")]
        [DefaultValue(1d)]
        public double Step
        {
            get { return this.step; }
            set { this.step = value; }
        }

        #endregion

        #region jQuery client events

        /// <summary>
        /// Defines the client events for the <see cref="Slider"/> control.
        /// </summary>
        public sealed class ClientEventDefinitions
        {
            #region properties

            [Setting("start")]
            internal ClientEventHandler StartHandler
            {
                get { return this.Start == null ? null : new ClientEventHandler("event,ui", this.Start); }
            }

            /// <summary>
            /// Gets or sets a javascript snippet, that is triggered when the user starts
            /// sliding. The names of the arguments to the function are <c>event</c> and
            /// <c>ui</c>.
            /// </summary>
            [Browsable(true)]
            [Localizable(false)]
            [EditorBrowsable(EditorBrowsableState.Always)]
            [Category("Behavior")]
            [PersistenceMode(PersistenceMode.InnerProperty)]
            public string Start
            {
                get;
                set;
            }

            [Setting("slide")]
            internal ClientEventHandler SlideHandler
            {
                get { return this.Slide == null ? null : new ClientEventHandler("event,ui", this.Slide); }
            }

            /// <summary>
            /// Gets or sets a javascript snippet, that is triggered on every mouse move
            /// during slide. Use <c>ui.value</c> (single-handled sliders) to obtain the
            /// value of the current handle, <c>$(this).slider('value', index)</c> to get
            /// another handles' value. The names of the arguments to the function are
            /// <c>event</c> and <c>ui</c>.
            /// 
            /// Return <c>false</c> in order to prevent a slide, based on <c>ui.value</c>.
            /// </summary>
            [Browsable(true)]
            [Localizable(false)]
            [EditorBrowsable(EditorBrowsableState.Always)]
            [Category("Behavior")]
            [PersistenceMode(PersistenceMode.InnerProperty)]
            public string Slide
            {
                get;
                set;
            }

            [Setting("change")]
            internal ClientEventHandler ChangeHandler
            {
                get { return this.Change == null ? null : new ClientEventHandler("event,ui", this.Change); }
            }

            /// <summary>
            /// Gets or sets a javascript snippet, that is triggered on slide stop, or if
            /// the value of the slider is changed programmatically (by the value method).
            /// It takes the two arguments <c>event</c> and <c>ui</c>. Use
            /// <c>event.orginalEvent</c> to detect whether the value was changed by mouse,
            /// keyboard, or programmatically. Use <c>ui.value</c> (single-handled sliders)
            /// to obtain the value of the current handle, <c>$(this).slider('values', index)</c>
            /// to get another handle's value.
            /// </summary>
            [Browsable(true)]
            [Localizable(false)]
            [EditorBrowsable(EditorBrowsableState.Always)]
            [Category("Behavior")]
            [PersistenceMode(PersistenceMode.InnerProperty)]
            public string Change
            {
                get;
                set;
            }

            [Setting("stop")]
            internal ClientEventHandler StopHandler
            {
                get { return this.Stop == null ? null : new ClientEventHandler("event,ui", this.Stop); }
            }

            /// <summary>
            /// Gets or sets a javascript snippet, that is triggered when the user stops
            /// sliding. The names of the arguments to the function are <c>event</c> and
            /// <c>ui</c>.
            /// </summary>
            [Browsable(true)]
            [Localizable(false)]
            [EditorBrowsable(EditorBrowsableState.Always)]
            [Category("Behavior")]
            [PersistenceMode(PersistenceMode.InnerProperty)]
            public string Stop
            {
                get;
                set;
            }

            #endregion

            #region constructors

            /// <summary>
            /// Creates a new <see cref="ClientEventDefinitions"/> instance.
            /// </summary>
            internal ClientEventDefinitions()
            {
            }

            #endregion
        }

        private readonly ClientEventDefinitions clientEvents = new ClientEventDefinitions();

        /// <summary>
        /// Holds the definitions for the client events.
        /// </summary>
        [SettingGroup]
        [PersistenceMode(PersistenceMode.InnerProperty)]
        public ClientEventDefinitions ClientEvents
        {
            get
            {
                return this.clientEvents;
            }
        }

        #endregion

        #region events

        /// <summary>
        /// This event is raised, just before the control applies its javascript
        /// settings to the initialization code. You can use it to manipulate the
        /// settings dictionary.
        /// </summary>
        /// <remarks>
        /// Be careful when using this event. It allows you to reach deeply into
        /// the internal workings of the control, possibly breaking functionality.
        /// </remarks>
        [EditorBrowsable(EditorBrowsableState.Advanced)]
        public event EventHandler<SettingsEventArgs> ApplyingSettings;

        /// <summary>
        /// Fires the <see cref="ApplyingSettings"/> event.
        /// </summary>
        /// <param name="e">The <see cref="SettingsEventArgs"/> passed to the event.</param>
        protected virtual void OnApplyingSettings(SettingsEventArgs e)
        {
            if (null != this.ApplyingSettings)
            {
                this.ApplyingSettings(this, e);
            }
        }

        #endregion

        #region page lifecycle

        /// <summary>
        /// Raises the <see cref="E:System.Web.UI.Control.Init"/> event.
        /// </summary>
        /// <param name="e">An <see cref="T:System.EventArgs"/> object that contains the event data.</param>
        protected override void OnInit(EventArgs e)
        {
            if (this.AutoRegister)
            {
                this.Page.RegisterJQueryUI();
            }

            base.OnInit(e);
        }

        /// <summary>
        /// Processes the postback data for the <see cref="T:System.Web.UI.WebControls.TextBox"/> control.
        /// </summary>
        /// <param name="postDataKey">The index within the posted collection that references the content to load.</param>
        /// <param name="postCollection">The collection posted to the server.</param>
        /// <returns><c>true</c> if the posted content is different from the last posting;
        /// otherwise, <c>false</c>.</returns>
        protected override bool LoadPostData(string postDataKey, System.Collections.Specialized.NameValueCollection postCollection)
        {
            bool result = base.LoadPostData(postDataKey, postCollection);
            // These fields always have to be reverted. base.LoadPostData will only return true,
            // if the value actually changed.
            this.setValue = null;
            this.setLowValue = null;
            this.setHighValue = null;
            return result;
        }

        /// <summary>
        /// Registers client script for generating postback events prior to rendering on the client, if <see cref="P:System.Web.UI.WebControls.TextBox.AutoPostBack"/> is true.
        /// </summary>
        /// <param name="e">An <see cref="T:System.EventArgs"/> object that contains the event data.</param>
        protected override void OnPreRender(EventArgs e)
        {
            base.OnPreRender(e);

            if (this.Visible)
            {
                bool isRange = this.Range == SliderRange.Range;
                Dictionary<string, object> settings = SettingSerializer.FetchSettings(this);
                if (isRange)
                {
                    settings["change"] = new ClientEventHandler(this.ClientEvents.ChangeHandler ?? new ClientEventHandler("event,ui", ""),
                                                                String.Format(CultureInfo.InvariantCulture,
                                                                              "jQuery('#{0}').val(ui.values[0] + ';' + ui.values[1]);",
                                                                              this.ClientID));
                    double lowValue = this.LowValue;
                    double highValue = this.HighValue;
                    settings["values"] = new List<double> { Math.Min(lowValue, highValue), Math.Max(lowValue, highValue) };
                }
                else
                {
                    settings["change"] = new ClientEventHandler(this.ClientEvents.ChangeHandler ?? new ClientEventHandler("event,ui", ""),
                                                                String.Format(CultureInfo.InvariantCulture,
                                                                              "jQuery('#{0}').val(ui.value);",
                                                                              this.ClientID));
                    settings["value"] = this.Value;
                }
                this.OnApplyingSettings(new SettingsEventArgs(settings));
                string settingsJson = SettingSerializer.Serialize(settings);
                this.Page.RegisterJQueryOnDocumentReadyBlock(String.Format(
                        CultureInfo.InvariantCulture,
                        "$('#{0}').slider({1});$('#{2}').hide()",
                        this.SliderClientId,
                        settingsJson,
                        this.ClientID
                        ));
            }
        }

        /// <summary>
        /// Renders the <see cref="Slider"/> control to the specified <see cref="T:System.Web.UI.HtmlTextWriter"/> object.
        /// </summary>
        /// <param name="writer">The <see cref="T:System.Web.UI.HtmlTextWriter"/> that receives the rendered output.</param>
        protected override void Render(HtmlTextWriter writer)
        {
            string themeName = this.ThemeName;
            if (String.IsNullOrEmpty(themeName))
            {
                themeName = this.Page.GetDefaultJQueryUITheme();
            }
            if (!String.IsNullOrEmpty(themeName))
            {
                writer.AddAttribute(HtmlTextWriterAttribute.Class, "jquery-ui-" + themeName);
                writer.RenderBeginTag(HtmlTextWriterTag.Div);
            }

            this.Text = this.Range != SliderRange.Range
                            ? this.Value.ToString(CultureInfo.InvariantCulture)
                            : String.Format(CultureInfo.InvariantCulture, "{0};{1}", this.LowValue, this.HighValue);
            base.Render(writer);

            writer.AddAttribute(HtmlTextWriterAttribute.Id, this.SliderClientId);
            if (!String.IsNullOrEmpty(this.CssClass))
            {
                writer.AddAttribute(HtmlTextWriterAttribute.Class, this.CssClass);
            }
            if (!this.Width.IsEmpty)
            {
                writer.AddStyleAttribute(HtmlTextWriterStyle.Width, this.Width.ToString(CultureInfo.InvariantCulture));
            }
            if (!this.Height.IsEmpty)
            {
                writer.AddStyleAttribute(HtmlTextWriterStyle.Height, this.Height.ToString(CultureInfo.InvariantCulture));
            }
            writer.RenderBeginTag(HtmlTextWriterTag.Div);
            writer.RenderEndTag();

            if (!String.IsNullOrEmpty(themeName))
            {
                writer.RenderEndTag();
            }
        }

        #endregion

        #region methods

        private double SanitizeValue(double value)
        {
            return Math.Min(Math.Max(value, this.Min), this.Max);
        }

        private void TryParseValue()
        {
            if (this.Range != SliderRange.Range)
            {
                double val;
                if (double.TryParse(this.Text, NumberStyles.Float, CultureInfo.InvariantCulture, out val))
                {
                    this.parsedValue = val;
                    return;
                }
            }
            this.parsedValue = 0;
        }

        private void TryParseLowHighValue()
        {
            if (this.Range == SliderRange.Range)
            {
                string[] parts = this.Text.Split(new[] {";"}, StringSplitOptions.RemoveEmptyEntries);
                if (parts.Length == 2)
                {
                    double lowVal;
                    if (double.TryParse(parts[0], NumberStyles.Float, CultureInfo.InvariantCulture, out lowVal))
                    {
                        double highVal;
                        if (double.TryParse(parts[1], NumberStyles.Float, CultureInfo.InvariantCulture, out highVal))
                        {
                            this.parsedLowValue = Math.Min(lowVal, highVal);
                            this.parsedHighValue = Math.Max(lowVal, highVal);
                            return;
                        }
                    }
                }
            }
            this.parsedLowValue = 0;
            this.parsedHighValue = 0;
        }

        #endregion
    }
}
